﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlServerCe;
using System.Data.Metadata.Edm;
using System.Data.EntityClient;
using System.Reflection;
using System.Data.Objects;
using System.IO;
using Kokomo.PeInspector.Reflection.Disassembly;
using Kokomo.PeInspector.Reflection;
using Kokomo.Mirror.CliMetadata;

namespace Kokomo.PeInspector.Data
{
	class AssemblyIndexCache
	{
		static AssemblyIndexCache()
		{
			CompileQueries();
		}

		#region Queries

		private static void CompileQueries()
		{
			_queryModuleByID = CompiledQuery.Compile<AssemblyIndexCacheContainer, int, ModuleFile>(
				(container, moduleID) =>
					container.ModuleFiles
					.FirstOrDefault(m => m.Id == moduleID)
					);
			_queryModuleByFileName = CompiledQuery.Compile<AssemblyIndexCacheContainer, string, ModuleFile>(
				(container, moduleFileName) =>
					container.ModuleFiles
					.FirstOrDefault(m => m.FileName == moduleFileName)
					);
			_queryModuleIDByFileNameDateTime = CompiledQuery.Compile<AssemblyIndexCacheContainer, string, DateTime, int>(
				(container, moduleFileName, moduleDateTime) =>
					container.ModuleFiles
					.Where(m => (m.FileName == moduleFileName) && (m.ModuleDateTime == moduleDateTime))
					.Select(m => m.Id)
					.FirstOrDefault()
					);
			_queryLastModuleID = CompiledQuery.Compile<AssemblyIndexCacheContainer, int?>(
				container => container.ModuleFiles.Max(m => (int?)m.Id)
					);
			_queryLastMemberDependencyID = CompiledQuery.Compile<AssemblyIndexCacheContainer, int?>(
				container => container.MemberDependencies.Max(m => (int?)m.RowId)
					);
			_queryLastStringDependencyID = CompiledQuery.Compile<AssemblyIndexCacheContainer, int?>(
				container => container.StringDependencies.Max(m => (int?)m.RowId)
					);
			_queryMemberDependencies = CompiledQuery.Compile<AssemblyIndexCacheContainer, int, int, IEnumerable<MemberDependency>>(
				(container, dependentModuleID, dependentToken) =>
					container.MemberDependencies.Where(m =>
						(m.DependentModuleId == dependentModuleID)
						&& (m.DependentToken == dependentToken)
					));
			_queryMemberDependents = CompiledQuery.Compile<AssemblyIndexCacheContainer, int, int, IEnumerable<MemberDependency>>(
				(container, dependencyModuleID, dependencyToken) =>
					container.MemberDependencies.Where(m =>
						(m.DependencyModuleId == dependencyModuleID)
						&& (m.DependencyToken == dependencyToken)
					));
		}

		private static Func<AssemblyIndexCacheContainer, int, ModuleFile> _queryModuleByID;
		public ModuleFile QueryModuleByID(int moduleID)
		{
			lock (this)
			{
				return _queryModuleByID(this._container, moduleID);
			}
		}

		private static Func<AssemblyIndexCacheContainer, string, ModuleFile> _queryModuleByFileName;
		public ModuleFile QueryModuleByFileName(string moduleFileName)
		{
			lock (this)
			{
				if (moduleFileName == null) throw new ArgumentNullException("moduleFileName");
				return _queryModuleByFileName(this._container, moduleFileName);
			}
		}

		private static Func<AssemblyIndexCacheContainer, string, DateTime, int> _queryModuleIDByFileNameDateTime;
		private int QueryModuleIDByFileNameDateTime(string moduleFileName, DateTime moduleDateTime)
		{
			lock (this)
			{
				if (moduleFileName == null) throw new ArgumentNullException("moduleFileName");
				return _queryModuleIDByFileNameDateTime(this._container, moduleFileName, moduleDateTime);
			}
		}

		private static Func<AssemblyIndexCacheContainer, int?> _queryLastModuleID;
		private int? LastModuleID()
		{
			lock (this)
			{
				return _queryLastModuleID(this._container);
			}
		}

		private static Func<AssemblyIndexCacheContainer, int?> _queryLastMemberDependencyID;
		internal int? LastMemberDependencyID()
		{
			lock (this)
			{
				return _queryLastMemberDependencyID(this._container);
			}
		}

		private static Func<AssemblyIndexCacheContainer, int?> _queryLastStringDependencyID;
		internal int? LastStringDependencyID()
		{
			lock (this)
			{
				return _queryLastStringDependencyID(this._container);
			}
		}

		private static Func<AssemblyIndexCacheContainer, int, int, IEnumerable<MemberDependency>> _queryMemberDependencies;
		public IEnumerable<MemberDependency> QueryDependenciesOf(int dependentModuleID, int dependentToken)
		{
			lock (this)
			{
				return _queryMemberDependencies(this._container, dependentModuleID, dependentToken);
			}
		}

		private static Func<AssemblyIndexCacheContainer, int, int, IEnumerable<MemberDependency>> _queryMemberDependents;
		public IEnumerable<MemberDependency> QueryDependentsOf(int dependencyModuleID, int dependencyToken)
		{
			lock (this)
			{
				return _queryMemberDependents(this._container, dependencyModuleID, dependencyToken);
			}
		}
		#endregion

		private const string ConnectionStringFormat = "data source={0};max database size=4091";

		public AssemblyIndexCache(
			string databaseFileName
			)
		{
			if (databaseFileName == null) throw new ArgumentNullException("databaseFileName");

			string connectionString = string.Format(ConnectionStringFormat, databaseFileName);
			SqlCeConnection connection = new SqlCeConnection(connectionString);
			MetadataWorkspace workspace = new MetadataWorkspace(
				new string[] { "res://Kokomo.PeInspector/" },
				new Assembly[] { this.GetType().Assembly }
				);
			EntityConnection entityConnection = new EntityConnection(workspace, connection);
			AssemblyIndexCacheContainer container = new AssemblyIndexCacheContainer(entityConnection);

			this._connection = connection;
			this._container = container;

			this._builder = new CacheBuilder(this, this._connection.ConnectionString);
		}

		public static AssemblyIndexCache Create(string databaseFileName)
		{
			if (databaseFileName == null) throw new ArgumentNullException("databaseFileName");

			Directory.CreateDirectory(Path.GetDirectoryName(databaseFileName));
			Stream templateStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(AssemblyIndexCacheContainer).Namespace + ".AssemblyIndexCache.sdf");
			using (FileStream stream = File.Create(databaseFileName))
			{
				int length = (int)templateStream.Length;
				byte[] data = new byte[length];
				templateStream.Read(data, 0, length);
				stream.Write(data, 0, length);
			}

			return new AssemblyIndexCache(databaseFileName);
		}

		private SqlCeConnection _connection;
		private AssemblyIndexCacheContainer _container;

		public string DatabaseFileName { get; private set; }

		public int RegisterModule(Module module)
		{
			if (module == null) throw new ArgumentNullException("module");

			string fileName = module.FullyQualifiedName;
			DateTime moduleDateTime = File.GetLastWriteTimeUtc(fileName);

			int moduleID;
			lock (this)
			{
				int? lastModuleID = this.LastModuleID();
				moduleID = (lastModuleID.HasValue ? lastModuleID.Value + 1 : 1);

				ModuleFile moduleFile = new ModuleFile()
				{
					Id = moduleID,
					FileName = fileName,
					ModuleDateTime = moduleDateTime
				};

				this._container.AddToModuleFiles(moduleFile);
				this._container.SaveChanges();
			}

			return moduleID;
		}

		public int GetModuleID(Module module)
		{
			if (module == null) throw new ArgumentNullException("module");

			string fileName = module.FullyQualifiedName;
			DateTime moduleDateTime = File.GetLastWriteTimeUtc(fileName);

			int moduleID;
			lock (this)
			{
				moduleID = this.QueryModuleIDByFileNameDateTime(
					fileName,
					moduleDateTime
					);

				if (moduleID == 0)
				{
					moduleID = this.RegisterModule(module);
				}
			}

			return moduleID;
		}

		private CacheBuilder _builder;

		public void CacheDependencies(IEnumerable<IGrouping<ModuleMemberReference, MsilDependency>> dependencyGroups)
		{
			if (dependencyGroups == null) throw new ArgumentNullException("dependencyGroups");

			CacheBuilder builder = this._builder;
			foreach (var dependencyGroup in dependencyGroups)
			{
				lock (this)
				{
					ModuleMemberReference dependent = dependencyGroup.Key;
					foreach (var dependency in dependencyGroup)
					{
						MsilMemberDependency memberDependency = dependency as MsilMemberDependency;
						if (memberDependency != null)
						{
							builder.AddMemberDependency(dependent, memberDependency);
						}
						else
						{
							MsilStringDependency stringDependency = dependency as MsilStringDependency;
							if (stringDependency != null)
							{
								builder.AddStringDependency(dependent, stringDependency);
							}
						}
					}
				}
			}
		}
	}
}
